在昨天有介紹到 x-template ,在這裡面提到了 props
,所以今天就來談談元件組溝通的方式及做法。
一個元件可以包含另一個或多個其他的元件,當一個元件包含另一個元件,它們就形成了父子關係,這也是最常見的組合,所以我們需要知道父子元件是如何溝通的。
在 Vue 中,父元件
將資料傳遞給子元件
的動作叫做 Pass Props
(又稱 Props down
),而由子元件
將資料傳遞給父元件
的動作叫做 Emit Events
(又稱 Events up)。
Props down
每個元件的作用域都是互相獨立互相不干涉的,意思是說我們不能(也不應該)在子元件中直接引用父元件的資料。因此父元件想要傳遞資料給子元件時,可以使用 props 屬性,將資料傳遞給子元件。
用 x-templat 來做例:
<div id="app">
<whoami :myname="name" area="鳳山"></whoami>
</div>
<script type="text/x-template" id="whoami">
<div>
<p>我是<strong>{{area}}</strong><em>{{myname}}</em></p>
</div>
</script>
<script>
Vue.component('whoami', {
props: ["myname", "area"],
template: '#whoami'
})
var app = new Vue({
el: '#app',
data: {
name: "金城武"
}
});
</script>
資料從最上層(Root)到最下層(子元件)的資料流:
- 我們建了一個全域元件
whoami
,在此模板使用時定義兩個屬性myname
、area
。whoami
裡有個可自訂屬性props
,它會去接收寫在模板的屬性myname
、area
上的資料。- 再借由
myname
、area
收到的資料用mustache
({{}}
),來渲染x-template
畫面。- 其中
myname
使用了v-bind
指令,為了把 vue instancedata
中的name
帶入。
因為 HTML 的屬性是不分大小寫的,所以元件中以駝峰式命名的 prop
,必須在 HTML中改用橫線連接的方式,或是改用其他方式命名:
<div id="vm">
<!-- 顯示:我是 -->
<my-profile myName="Tony"></my-profile>
<!-- 顯示:我是 Tony -->
<my-profile my-name="Tony"></my-profile>
</div>
<script>
Vue.component('my-profile', {
props: ['myName'],
template: '<p>我是 {{ myName }}</p>'
})
new Vue({
el: '#vm'
})
</script>
註:使用字串模板,就沒有這個限制。
我們可以對父元件的資料做些手腳,讓父元件資料的改變間接的影響子元件來達到動態的效果。如果我們要達到這個效果就要用到 v-bind
和資料雙向綁定的指令 v-model
讓子元件可以動態的變更資料:
<div id="app">
<input type="text" v-model="name">
<whoami :myname="name" area="鳳山"></whoami>
</div>
<script type="text/x-template" id="whoami">
<div>
<p>我是<strong>{{area}}</strong><em>{{myname}}</em></p>
</div>
</script>
<script>
Vue.component('whoami', {
props: ["myname", "area"],
template: '#whoami'
})
var app = new Vue({
el: '#app',
data: {
name: "金城武"
}
});
</script>
Props 是單向綁定的,也就是說當父元件的屬性發生變動,會立即將變動的資料傳給子元件,這個方向只能單向的傳資料給子元件,相反過來是不行的。
以下範例會報錯誤:
<div id="app">
<photo :img-url="url"></photo>
<p>修正單向數據流所造成的錯誤</p>
</div>
<script type="text/x-template" id="photo">
<div>
<img :src="imgUrl"/>
<input type="text" v-model="imgUrl">
</div>
</script>
Vue.component('photo', {
props: ['imgUrl'],
template: '#photo',
})
Vue.component('card', {
props: ['userData'],
template: '#card',
data: function() {
return {
user: this.userData
}
}
});
var app = new Vue({
el: '#app',
data: {
url: 'https://images.unsplash.com/photo-1522204538344-922f76ecc041?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=50e38600a12d623a878983fc5524423f&auto=format&fit=crop&w=1351&q=80',
},
});
解法是我們在元件內使用 data 這個屬性,來接外部傳進來的內容:
<script type="text/x-template" id="photo">
<div>
<img :src="imgUrl"/>
// 使用新的 newUrl ,這個 url 跟父元素沒有直接綁定
<input type="text" v-model="newUrl">
</div>
</script>
Vue.component('photo', {
props: ['imgUrl'],
template: '#photo',
data: function() {
return {
// this.imgUrl 是 props 所傳進來的,我們定義一個新的 url 給他,
// 讓他不要跟外部的資料做綁定。
newUrl: this.imgUrl,
}
}
})
我們的 card
元件從父元件傳過去的資料是經由 AJAX 方式傳送。在父元件會先宣告一個 user
物件,然後透過 AJAX 行為把遠端的資料抓進來之後,才把它存進 user
物件裡面,在這之中會有時間差(因為執行的東西會先放在 event queue),所以 card
元件會報錯說找不到資料:
<div id="app">
<div class="row">
<div class="col-sm-4">
<card :user-data="user"></card>
</div>
</div>
</div>
<script type="text/x-template" id="card">
<div class="card">
<img class="card-img-top" :src="user.picture.large" alt="Card image cap">
<div class="card-body">
<h5 class="card-title">{{ user.name.first }} {{ user.name.last }}</h5>
<p class="card-text">{{ user.email }}</p>
</div>
<div class="card-footer">
<input type="email" class="form-control" v-model="user.email">
</div>
</div>
</script>
Vue.component('card', {
props: ['userData'],
template: '#card',
data: function() {
return {
user: this.userData
}
}
});
var app = new Vue({
el: '#app',
data: {
user: {},
},
created: function() {
var vm = this;
$.ajax({
url: 'https://randomuser.me/api/',
dataType: 'json',
success: function(data) {
vm.user = data.results[0];
}
});
}
});
這裏我們可以用簡單的方式來解決這個問題,使用 v-if
假設 user 某個特性並沒有完全讀進來之前,就不要把這個卡片執行出來:
<div id="app">
<div class="row">
<div class="col-sm-4">
// 假設它有電話號碼就把資料繪製出來
<card :user-data="user" v-if="user.phone"></card>
</div>
</div>
</div>
這樣就可以正常顯示了,這裏的觀念是如果資料匯入會有時間差,可以用 v-if
讓元件的產生往後移,與資料完成的時候一起同步繪製。
再來就是我們的 user
是個物件,因為物件是傳參考所以當我們在子元件修改資料的時候也會動到父元件的資料,這個是 JS 的特性,有此可知我們統整一下有幾種方式可以來打破子元件禁制回傳資料到父元件這件事:
我們可以在元件定義 Prop 的資料型態,當傳入的資料不符合該型態時,Vue就會提出警告。
通常會使用物件的方式來定義資料型態,但是如果有多個資料型態,可以使用陣列。
Vue.component('child', {
props: {
// 直接定義屬性
id: Number,
// 使用陣列來定義多個型態
name: [String, Number],
// 使用物件來寫一些規則
age: {
type: String,
default: '',
validator: function(value) {
return value > 0;
},
required: true
}
}
})
物件中的規則說明如下:
type
:資料型態default
:預設值validator
:檢驗屬性資料是否有效required
:該屬性是否為必要type可以有的屬性如下:
String
Number
Boolean
Function
Object
Array
Symbol
type
可以是自訂的建構式函式,使用 instanceof
來檢測。
注意,因為元件會在 vue instance 創建之前建立好,所以在 default
或 validator
函數裡,不能使用data
、computed
、methods
等屬性無法使用。
Day17 - [Components] 元件組合與溝通
Vue.js (9.1) - 元件(Component)